﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Globalization;

namespace BurnSystems.Gpx
{
    /// <summary>
    /// A geographic point with optional elevation and time. Available for use by other schemas. 
    /// </summary>
    public class GpxPoint
    {
        /// <summary>
        /// Stores the node
        /// </summary>
        protected XElement node;

        /// <summary>
        /// Initializes a new instance of the GpxTrack class.
        /// </summary>
        /// <param name="node">Node to be used for track</param>
        public GpxPoint(XElement node)
        {            
            this.node = node;
        }

        /// <summary>
        /// Gets or sets the latitude
        /// </summary>
        public double Latitude
        {
            get
            {
                return Convert.ToDouble(this.node.GetAttributeContent("lat", "0"), CultureInfo.InvariantCulture);
            }

            set
            {
                this.node.SetAttributeContent("lat", value.ToString(CultureInfo.InvariantCulture));
            }
        }

        /// <summary>
        /// Gets or sets the longitude
        /// </summary>
        public double Longitude
        {
            get
            {
                return Convert.ToDouble(this.node.GetAttributeContent("lon", "0"), CultureInfo.InvariantCulture);
            }

            set
            {
                this.node.SetAttributeContent("lon", value.ToString(CultureInfo.InvariantCulture));
            }
        }

        /// <summary>
        /// Gets or sets the elevation
        /// </summary>
        public double Elevation
        {
            get
            {
                return Convert.ToDouble(this.node.GetElementContent("ele", "0"), CultureInfo.InvariantCulture);
            }

            set
            {
                this.node.SetElementContent("ele", value.ToString(CultureInfo.InvariantCulture));
            }
        }

        /// <summary>
        /// Gets or sets the time
        /// </summary>
        public DateTime Time
        {
            get
            {
                return Convert.ToDateTime(this.node.GetElementContent("time", "0"), CultureInfo.InvariantCulture);
            }

            set
            {
                this.node.SetElementContent("time", value.ToString(CultureInfo.InvariantCulture));
            }
        }

        /// <summary>
        /// Loads the information about the point by an xelement
        /// </summary>
        /// <param name="element">Element, from which the information will be read</param>
        public void LoadFromXElement(XElement element)
        {
            this.Latitude = Convert.ToDouble(
                element.Attribute("lat").Value, 
                CultureInfo.InvariantCulture);
            this.Longitude = Convert.ToDouble(
                element.Attribute("lon").Value, 
                CultureInfo.InvariantCulture);

            if (element.Element(GpxDocument.FullyQualifiedNS + "ele") != null)
            {
                this.Elevation = Convert.ToDouble(element.Element(GpxDocument.FullyQualifiedNS + "ele").Value);
            }

            if (element.Element(GpxDocument.FullyQualifiedNS + "time") != null)
            {
                this.Time = Convert.ToDateTime(
                    element.Element(GpxDocument.FullyQualifiedNS + "time").Value, 
                    CultureInfo.InvariantCulture);
            }
        }

        /// <summary>
        /// Converts the point to string
        /// </summary>
        /// <returns></returns>
        public override string ToString()
        {
            return string.Format("{0}, {1}", this.Latitude, this.Longitude);
        }
        
        /// <summary>
        /// Stores the earth radius in kilometers
        /// </summary>
        public const double EarthRadius = 6367000;

        /// <summary>
        /// Calculates the distance between the two points
        /// </summary>
        /// <param name="point1">First point</param>
        /// <param name="point2">Second point</param>
        /// <returns>Distance between these nodes</returns>
        public static double GetDistance(GpxPoint point1, GpxPoint point2)
        {
            var lat1 = point1.Latitude;
            var lng1 = point1.Longitude;

            var lat2 = point2.Latitude;
            var lng2 = point2.Longitude;

            return EarthRadius * 2 * Math.Asin(Math.Min(1, Math.Sqrt((Math.Pow(Math.Sin((DifferenceInRadian(lat1, lat2)) / 2.0), 2.0) + Math.Cos(ToRadian(lat1)) * Math.Cos(ToRadian(lat2)) * Math.Pow(Math.Sin((DifferenceInRadian(lng1, lng2)) / 2.0), 2.0))))); 
        }

        /// <summary>
        /// Converts the given angle to a radian
        /// </summary>
        /// <param name="value">Value to be converted</param>
        /// <returns>Converted angle</returns>
        private static double ToRadian(double value)
        {
            return value * (Math.PI / 180);
        }

        /// <summary>
        /// Calculates the radien difference of two degree angles
        /// </summary>
        /// <param name="value1">First value</param>
        /// <param name="value2">Second value</param>
        /// <returns>Converted angle</returns>
        private static double DifferenceInRadian(double value1, double value2)
        {
            return ToRadian(value2) - ToRadian(value1);
        }
    }
}
